function Compile(el, vm) {
  // el=> '#app'
  // vm ==> vue实例
  // 将vue实例存储起来
  this.$vm = vm
  // 先判断el是否是dom元素,不是的话,则根据选择器获取dom元素
  // this.$el最终应该是模板的dom元素
  this.$el = this.isElementNode(el) ? el : document.querySelector(el)
  // 有可能选择器写错了,则无法获取到模板的dom元素,所以判断一下
  if (this.$el) {
    // 将dom树上的节点添加到文档碎片中
    this.$fragment = this.node2Fragment(this.$el)
    // 真正开始解析模板,调用init
    this.init()
    // 模板解析完毕之后,将文档碎片中的节点重新添加到dom树上
    this.$el.appendChild(this.$fragment)
  }
}

Compile.prototype = {
  //  将dom树上的节点添加到文档碎片中
  node2Fragment: function (el) {
    // 创建文档碎片
    var fragment = document.createDocumentFragment(),
      child
    // 循环遍历dom树中模板对应的节点里面的所有子节点,一个一个的添加到文档碎片中
    // 由于添加到文档碎片中的节点在dom树上不存在了,所以第一个添加到文档碎片之后,第二个节点就变成了第一个节点,所以这里一直获取firstChild
    while ((child = el.firstChild)) {
      fragment.appendChild(child)
    }
    // 返回文档碎片
    return fragment
  },

  init: function () {
    // compileElement是具体编译模板的代码
    // this.$fragment 是文档碎片
    this.compileElement(this.$fragment)
  },

  compileElement: function (el) {
    // 获取文档随便中所有的节点
    var childNodes = el.childNodes,
      // 存储当前编译的实例对象,便于后面使用
      me = this
    // 将文档随便中存储节点的伪数组转成真数组,然后遍历
    ;[].slice.call(childNodes).forEach(function (node) {
      // 获取节点的内容
      var text = node.textContent
      // 定义正则,这个正则用于匹配插值表达式 '{{}}'
      var reg = /\{\{(.*)\}\}/
      // 判断当前节点是否是元素节点
      if (me.isElementNode(node)) {
        // 如果是,则调用complie解析元素上的属性
        me.compile(node)
        // 当前节点不是元素节点,继续判断是否是文本节点,以及这个文本节点中是否写了插值表达式
      } else if (me.isTextNode(node) && reg.test(text)) {
        // 如果是文本节点,并写的插值表达式,则调用compileText解析插值表达式
        me.compileText(node, RegExp.$1)
      }

      // 判断当前节点,是否还有子节点,如果有,则递归遍历子节点
      if (node.childNodes && node.childNodes.length) {
        me.compileElement(node)
      }
    })
  },

  compile: function (node) {
    // node是当前元素节点
    // 获取当前元素节点所有的属性(包含自定义属性)
    // 注意: nodeAttrs是一个伪数组,里面存储的是属性对象
    var nodeAttrs = node.attributes,
      me = this
    // 把存储了属性对象的伪数组,转成真数组,进行遍历
    ;[].slice.call(nodeAttrs).forEach(function (attr) {
      // attr是属性对象
      // attrName存储是当前属性对象的name属性的值,说白了就是属性名(字符串)
      var attrName = attr.name
      // 判断当前属性名是否是指令(判断依据: 是否以v-开头)
      if (me.isDirective(attrName)) {
        // 如果是指令,则获取当前指令对象的值(其实就是模板中指令后面的引号的内容)
        var exp = attr.value
        // 将指令字符串的v-去掉
        var dir = attrName.substring(2)
        // 判断去掉v-之后的名字是否是事件指令(判断依据是否以on开头)
        if (me.isEventDirective(dir)) {
          // 处理事件指令
          compileUtil.eventHandler(node, me.$vm, exp, dir)
        } else {
          // 处理其他指令
          compileUtil[dir] && compileUtil[dir](node, me.$vm, exp)
        }
        node.removeAttribute(attrName)
      }
    })
  },

  compileText: function (node, exp) {
    // 调用其他方法,处理插值表达式的数据
    compileUtil.text(node, this.$vm, exp)
  },

  isDirective: function (attr) {
    return attr.indexOf('v-') == 0
  },

  isEventDirective: function (dir) {
    return dir.indexOf('on') === 0
  },

  isElementNode: function (node) {
    return node.nodeType == 1
  },

  isTextNode: function (node) {
    return node.nodeType == 3
  },
}

var compileUtil = {
  // v-text指令走这里
  text: function (node, vm, exp) {
    // 继续调用
    this.bind(node, vm, exp, 'text')
  },
  // v-html指令走这里
  html: function (node, vm, exp) {
    this.bind(node, vm, exp, 'html')
  },

  model: function (node, vm, exp) {
    this.bind(node, vm, exp, 'model')

    var me = this,
      val = this._getVMVal(vm, exp)
    node.addEventListener('input', function (e) {
      // 10. 当用户在文本框中输入新的值,则这个input事件触发,意味着要修改msg的值了
      // 获取用户输入的最新的数据
      var newValue = e.target.value
      // 判断新值和旧值是否相同,如果不同则修改数据,如果相同则什么都不做
      if (val === newValue) {
        return
      }
      // 11. 调用compile实例的方法,去修改msg的值
      me._setVMVal(vm, exp, newValue)
      val = newValue
    })
  },

  class: function (node, vm, exp) {
    this.bind(node, vm, exp, 'class')
  },

  bind: function (node, vm, exp, dir) {
    // 继续调用,找到具体处理这个插值表达式的函数
    // 这个updaterFn只是一个变量,用来存储处理插值表达式或其他指令的函数
    // 如果只是处理插值表达式,则updaterFn中存储的这个函数名叫做textUpdater函数
    var updaterFn = updater[dir + 'Updater']
    // 判断是否找到了对应的函数,然后调用这个函数
    // 因为在textUpdater中要具体讲数据的值插入到模板中,所以this._getVMVal(vm, exp)是根据插值表达
    // 式中的表达式获取对应的数据(this._getVMVal可以暂时跳过)
    updaterFn && updaterFn(node, this._getVMVal(vm, exp))
    // 这个new Watcher是用来绑定数据和模板(暂时跳过)
    // 1. 数据和模板建立绑定关系是从new Watcher开始的
    new Watcher(vm, exp, function (value, oldValue) {
      // 这个回调是当msg的数据发生变化的时候触发
      // 20. 更新msg对应的模板(updaterFn此时是modelUpdater)
      updaterFn && updaterFn(node, value, oldValue)
    })
  },

  eventHandler: function (node, vm, exp, dir) {
    // 将指令继续切换,最终获取到事件名
    // eventType 是事件名(字符串)
    var eventType = dir.split(':')[1],
      // 获取事件处理函数(根据exp去options中获取对应的函数)
      // fn就是当前绑定事件的事件处理函数
      fn = vm.$options.methods && vm.$options.methods[exp]
    // 如果事件名和事件处理函数都找到了,就直接绑定事件
    if (eventType && fn) {
      //fn.bind(vm) 这里利用事件处理函数,克隆一个新的函数,并指定新函数中this指向vm实例.这个新函数才是
      //真正的事件处理函数
      node.addEventListener(eventType, fn.bind(vm), false)
    }
  },

  _getVMVal: function (vm, exp) {
    var val = vm._data
    exp = exp.split('.')
    exp.forEach(function (k) {
      val = val[k]
    })
    return val
  },

  // 12. 继续往下执行,去修改msg的值
  _setVMVal: function (vm, exp, value) {
    var val = vm._data
    exp = exp.split('.')
    exp.forEach(function (k, i) {
      if (i < exp.length - 1) {
        val = val[k]
      } else {
        // 13. 触发数据劫持的set,对msg的值进行修改
        val[k] = value
      }
    })
  },
}

var updater = {
  // 插值表达式和v-text都执行这个函数
  textUpdater: function (node, value) {
    // node就是节点,value就是这个节点写的插值表达式的值
    // 将具体的值,赋值给当前节点.同时做了一个简单的判断,如果值是undefined,则不渲染
    node.textContent = typeof value == 'undefined' ? '' : value
  },
  // v-html执行这个函数
  htmlUpdater: function (node, value) {
    node.innerHTML = typeof value == 'undefined' ? '' : value
  },

  classUpdater: function (node, value, oldValue) {
    var className = node.className
    className = className.replace(oldValue, '').replace(/\s$/, '')

    var space = className && String(value) ? ' ' : ''

    node.className = className + space + value
  },
  // 21. 触发这个函数,将绑定了msg这个数据的节点的value值改为最新的msg的值
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value
  },
}
